/**
 * 数据类型 data-type
 * 6种简单数据类型：Undefined、Null、Boolean、Number、String 和 Symbol
 * Symbol 是 ES6 新增的数据类型，用于创建独一无二的值
 * 复杂数据类型：Object(包含Array)，Function
 * "undefined"表示值未定义；
   "boolean"表示值为布尔值；
   "string"表示值为字符串；
   "number"表示值为数值；
   "object"表示值为对象（而不是函数）或 null；
   "function"表示值为函数；
   "symbol"表示值为符号。
 */

// typeof操作符，返回字符串，字符串里是变量的类型
const foo = "some string";
console.log(typeof foo); // "string"
console.log(typeof (foo)); // "string"
console.log(typeof 99); // "number"
console.log(typeof true); // "boolean"
console.log(typeof null); // "object"
console.log(typeof undefined); // "undefined"
console.log(typeof Symbol("foo")); // "symbol"

// Undefined类型，只有一个值，就是undefined
let foo2;
console.log(foo2); // undefined
console.log(foo2 == undefined); // true
console.log(foo2 === undefined); // true

// Null类型，只有一个值，就是null，表示一个空指针
let foo3 = null;
console.log(typeof foo3); // "object"
console.log(null == undefined); // true

// Boolean类型，只有两个值，true和false
let foo4 = true;
console.log(foo4); // true
/**
 * 数据类型              转换为true的值               转换为false的值
 * Boolean              true                        false
 * String               非空字符串                   ""(空字符串)
 * Number               非零数值(包括无穷值)          0、NaN
 * Object               任意对象                     null
 * Undefined            N/A(不存在)                  undefined
 */

/**
 * Number类型，使用 IEEE 754 格式表示整数和浮点值（在某些语言中也叫双精度值）
 * 不同的数值类型相应地有不同的数值字面量格式
 * 由于 JavaScript 保存数值的方式，实际中可能存在正零（ +0）和负零（ -0）。正零和负零在所有情况下都被认为是等同的，这里特地说明一下。
 */
const foo5 = 99; // 整数
const foo6 = 0o70; // 八进制的 56
const foo7 = 0b1001; // 二进制的 9
const foo8 = 0x63; // 十六进制的 99

/**
 * 浮点值
 * 要定义浮点值，数值中必须包含小数点，而且小数点后面必须至少有一个数字
 * 虽然小数点前面不是必须有整数，但推荐加上
 * 浮点值的精确度最高可达 17 位小数
 * 0.1 + 0.2 不是 0.3， 而是 0.30000000000000004
 * 0.05 和 0.25，或者 0.15 和 0.15 是没问题的
 * 永远不要测试某个特定的浮点值
 * 浮点值的最小值是 Number.MIN_VALUE 5e-324
 * 浮点值的最大值是 Number.MAX_VALUE  1.797 693 134 862 315 7e+308
 * Infinity（无穷）值。任何无法表示的负数以-Infinity（负无穷大）表示，任何无法表示的正数以 Infinity（正无穷大）
 * isFinite() 用于检测是否是有限的（正值或负值）
 * NaN（意思是“不是数值”（ Not a Number）)。任何涉及 NaN 的操作始终返回 NaN（如 NaN/10）
 * NaN 不等于包括 NaN 在内的任何值
 * isNaN()判断这个参数是否“不是数值”
 */
const foo9 = 1.2;
const foo10 = .6; // 有效，但不推荐
const foo11 = 1.; // 小数点后面没有数字，当成整数 1 处理
const foo12 = 10.0; // 小数点后面是零，当成整数 10 处理
const foo13 = 3.125e7; // 科学记数法，等于 31250000
console.log(0 / 0); // NaN
console.log(-0 / +0); // NaN
console.log(5 / 0); // Infinity
console.log(5 / -0); // -Infinity
console.log(NaN == NaN); // false

console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false， 10 是数值
console.log(isNaN("10")); // false，可以转换为数值 10
console.log(isNaN("blue")); // true，不可以转换为数值
console.log(isNaN(true)); // false，可以转换为数值 1

/**
 * 数值转换
 * 3个函数将非数值转换为数值：Number()、parseInt() 和 parseFloat()
 * Number()：
 * 布尔值， true 转换为 1， false 转换为 0
 * 数值，直接返回
 * null，返回 0
 * undefined，返回 NaN
 * 字符串包含数值字符，包括数值字符前面带加、减号的情况，则转换为一个十进制数值
 * 字符串包含有效的浮点值格式如"1.1"，则会转换为相应的浮点值（同样，忽略前面的零）
 * 字符串包含有效的十六进制格式如"0xf"，则会转换为与该十六进制值对应的十进制整数值
 * 空字符串（不包含字符），则返回 0
 * 字符串包含除上述情况之外的其他字符，则返回 NaN
 * 
 * parseInt()函数更专注于字符串是否包含数值模式
 * 比如"1234blue"会被转换为 1234，"22.5"会被转换为 22
 * 
 * parseFloat()函数与parseInt()类似
 */
const num1 = parseInt("1234blue"); // 1234
const num2 = parseInt(""); // NaN
const num3 = parseInt("0xA"); // 10，解释为十六进制整数
const num4 = parseInt(22.5); // 22
const num5 = parseInt("70"); // 70，解释为十进制值
const num6 = parseInt("0xf"); // 15，解释为十六进制整数
const num7 = parseInt("0xAF", 16); // 175，接收第二个参数，用于指定底数（进制数）
const num8 = parseInt("10", 2); // 2，按二进制解析
const num9 = parseInt("10", 8); // 8，按八进制解析
const num10 = parseInt("10", 10); // 10，按十进制解析
const num11 = parseInt("10", 16); // 16，按十六进制解析
const num12 = parseFloat("1234blue"); // 1234，按整数解析
const num13 = parseFloat("0xA"); // 0
const num14 = parseFloat("22.5"); // 22.5
const num15 = parseFloat("22.34.5"); // 22.34
const num16 = parseFloat("0908.5"); // 908.5
const num17 = parseFloat("3.125e7"); // 31250000


/**
 * 字符串类型
 * 表示零或多个 16 位 Unicode 字符序列
 * 使用双引号（ "）、单引号（ '）或反引号（ `）标识
 */
const str = 'MasterH';
const str2 = "MasterH";
const str3 = `MasterH`; // ES6 新增字符串模板，可以插入变量
const str4 = 'MasterX';
const str5 = `Hello，${str4}`;

/**
 * 字符字面量
 * \n 换行，\t 制表符，\b 退格，\r 回车，\f 换页，\' 单引号，\" 双引号，\\ 反斜杠
 * \xXX 用指定的 16 位字符，以十六进制编码 nn 表示的字符（其中 n 是十六进制数字 0~F），例如\x41 等于"A"
 * \uXXXX 用指定的 16 位 Unicode 字符，以十六进制编码 nnnn 表示的 Unicode 字符（其中 n 是十六进制数字 0~F），例如\u03a3 等于希腊字符"Σ"
 */
console.log('\x52'); // 字母R
console.log('\u03a3'); // Σ

/**
 * toString()和String()方法，
 * toString()接收一个底数参数，即以什么底数来输出数值的字符串表示，默认是十进制
 * 不确定null或undefined，用String()方法
 * String(null)返回"null"字符串
 * String(undefined)返回"undefined"字符串
 */
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"

/**
 * 原始字符串
 */
// Unicode 示例
// \u00A9 是版权符号
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9



/**
 * Symbol 类型
 * 符号是原始值，且符号实例是唯一、不可变的。
 * 符号的用途是确保对象属性使用唯一标识符，不会发生属性冲突的危险。
 * Symbol()函数不能与 new 关键字一起作为构造函数使用，这样做是为了避免创建符号包装对象，即不能 new Symbol()!!!
 */
let sym = Symbol();
console.log(typeof sym); // symbol

let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo'); // foo只是个参数，与符号定义或标识完全无关
let otherFooSymbol = Symbol('foo');
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false

/**
 * 使用全局符号注册表
 * Symbol.for()方法可以将一个字符串作为参数，返回一个全局符号
 */
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
let s = Symbol.for('foo'); // 创建全局符号
console.log(Symbol.keyFor(s)); // foo
let s2 = Symbol('bar'); // 创建普通符号
console.log(Symbol.keyFor(s2)); // undefined

/**
 * 凡是可以使用字符串或数值作为属性的地方，都可以使用符号
 * s3、s4、s5、s6作为key键
 */
let s3 = Symbol('foo'),
  s4 = Symbol('bar'),
  s5 = Symbol('baz'),
  s6 = Symbol('qux');

let o = {
  [s3]: 'foo val'
}; // 这样也可以： o[s1] = 'foo val';
console.log(o); // {Symbol(foo): foo val}

Object.defineProperty(o, s2, { value: 'bar val' });
console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val}

Object.defineProperties(o, {
  [s3]: { value: 'baz val' },
  [s4]: { value: 'qux val' }
});
console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val, Symbol(baz): baz val, Symbol(qux): qux val}

/**
 * 常用内置符号（ well-known symbol），所有内置符号属性都是不可写、不可枚举、不可配置的
 * Symbol.iterator 用于获取对象的迭代器
 * Symbol.asyncIterator 用于获取对象的异步迭代器，这个符号作为一个属性表示“一个方法，该方法返回对象默认的 AsyncIterator。由 for-await-of 语句使用”
 * Symbol.hasInstance 用于判断一个构造函数的实例是否是该构造函数
 * Symbol.isConcatSpreadable 用于判断一个对象是否可以被Array.prototype.concat()方法展开
 * Symbol.match 用于判断一个对象是否可以被String.prototype.match()方法匹配
 * Symbol.replace 用于判断一个对象是否可以被String.prototype.replace()方法替换
 * Symbol.toStringTag 用于获取对象的类型描述
 * Symbol.unscopables 用于获取或设置对象的可作用域属性
 * Symbol.species 用于获取一个对象的构造函数
 */
// Symbol.asyncIterator
class Foo {
  async *[Symbol.asyncIterator]() { }
}
let f = new Foo();
console.log(f[Symbol.asyncIterator]()); // AsyncGenerator {<suspended>}

class Emitter {
  constructor(max) {
    this.max = max;
    this.asyncIdx = 0;
  }
  async *[Symbol.asyncIterator]() {
    while (this.asyncIdx < this.max) {
      yield new Promise((resolve) => resolve(this.asyncIdx++));
    }
  }
}
async function asyncCount() {
  let emitter = new Emitter(5);
  for await (const x of emitter) {
    console.log(x);
  }
}
asyncCount();
// 0
// 1
// 2
// 3
// 4

/**
 * Symbol.hasInstance
 * 这个符号作为一个属性表示“一个方法，该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用”。
 * instanceof 操作符可以用来确定一个对象实例的原型链上是否有原型。 
 */
function Foo() { }
let f2 = new Foo();
console.log(Foo[Symbol.hasInstance](f2)); // true
class Bar { }
let b = new Bar();
console.log(Bar[Symbol.hasInstance](b)); // true

class Bar { }
class Baz extends Bar {
  static [Symbol.hasInstance]() { // 重新定义这个函数
    return false;
  }
}
let b2 = new Baz();
console.log(Bar[Symbol.hasInstance](b2)); // true
console.log(b instanceof Bar); // true
console.log(Baz[Symbol.hasInstance](b2)); // false
console.log(b instanceof Baz); // false

/**
 * Symbol.isConcatSpreadable
 * 这个符号作为一个属性表示“一个布尔值，如果是 true，则意味着对象应该用 Array.prototype.concat()打平其数组元素”
 * 默认值是undefined，打平数组
 */
let initial = ['foo'];
let array = ['bar'];
console.log(array[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(array)); // ['foo', 'bar']
array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // ['foo', Array(1)] 不打平数组

let arrayLikeObject = { length: 1, 0: 'baz' }; // 类似于数组的对象，类数组对象
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]
arrayLikeObject[Symbol.isConcatSpreadable] = true; // 设置为true，打平数组
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']

let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set(1)] 默认追加整个对象
otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // ['foo'] 被忽略，Set不是数组，也不是类数组对象，即使设置true，也不会打平

/**
 * Symbol.iterator
 * 一个方法，该方法返回对象默认的迭代器。由 for-of 语句使用
 */
class Foo {
  *[Symbol.iterator]() { }
}
let f3 = new Foo();
console.log(f3[Symbol.iterator]()); // Generator {<suspended>}

class Emitter {
  constructor(max) {
    this.max = max;
    this.idx = 0;
  }
  *[Symbol.iterator]() {
    while (this.idx < this.max) {
      yield this.idx++;
    }
  }
}
function count() {
  let emitter = new Emitter(5);
  for (const x of emitter) {
    console.log(x);
  }
}
count();
// 0
// 1
// 2
// 3
// 4

/**
 * Symbol.match
 * 一个正则表达式方法，该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用
 */
console.log(RegExp.prototype[Symbol.match]); // ƒ [Symbol.match]() { [native code] }
console.log('foobar'.match(/bar/)); // ["bar", index: 3, input: "foobar", groups: undefined]
class FooMatcher {
  static [Symbol.match](target) {
    return target.includes('foo');
  }
}
console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false
class StringMatcher {
  constructor(str) {
    this.str = str;
  }
  [Symbol.match](target) {
    return target.includes(this.str);
  }
}
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false

class FooMatcher {
  static [Symbol.match](target) {
    return target.includes('foo');
  }
}
console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false
class StringMatcher {
  constructor(str) {
    this.str = str;
  }
  [Symbol.match](target) {
    return target.includes(this.str);
  }
}
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false

/**
 * Symbol.replace
 * Symbol.search
 * Symbol.species
 * Symbol.split
 * Symbol.toPrimitive
 * Symbol.toStringTag
 * Symbol.unscopables
 * 余下的这些符号使用规则请参考MDN文档
 * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Symbol
 */



/**
 * Object类型，Object 是所有对象的基类，
 * 属性和方法有
 * constructor：用于创建当前对象的函数。在下面的例子中，这个属性的值就是 Object()函数
 * hasOwnProperty(propertyName)：用于判断当前对象实例（不是原型）上是否存在给定的属性。要检查的属性名必须是字符串（如 o.hasOwnProperty("name")）或符号
 * isPrototypeOf(object)：用于判断当前对象是否为另一个对象的原型
 * propertyIsEnumerable(propertyName)：用于判断给定的属性是否可以使用 for-in 语句枚举。与 hasOwnProperty()一样，属性名必须是字符串
 * toLocaleString()：返回对象的字符串表示，该字符串反映对象所在的本地化执行环境
 * toString()：返回对象的字符串表示
 * valueOf()：返回对象对应的字符串、数值或布尔值表示。通常与 toString()的返回值相同
 * 
 */
const o1 = new Object();
const o2 = new Object; // 合法，但不推荐
